Skip to content

refactor(utils): simplify formatting error handling - #189

Closed
yugesh-ganipudi wants to merge 1 commit into
juspay:releasefrom
yugesh-ganipudi:BZ-7308187-Standardize-the-error-handling-across-providers
Closed

yugesh-ganipudi wants to merge 1 commit into
juspay:releasefrom
yugesh-ganipudi:BZ-7308187-Standardize-the-error-handling-across-providers

Conversation

@yugesh-ganipudi

@yugesh-ganipudi yugesh-ganipudi commented Sep 25, 2025 •

Copy link
Copy Markdown
Contributor

Pull Request

Description

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🧹 Code refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧪 Test coverage improvement
  • 🔧 Build/CI configuration change

Related Issues

  • Fixes #
  • Related to #

Changes Made

AI Provider Impact

  • OpenAI
  • Anthropic
  • Google AI/Vertex
  • AWS Bedrock
  • Azure OpenAI
  • Hugging Face
  • Ollama
  • Mistral
  • All providers
  • No provider-specific changes

Component Impact

  • CLI
  • SDK
  • MCP Integration
  • Streaming
  • Tool Calling
  • Configuration
  • Documentation
  • Tests

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • E2E tests added/updated
  • Manual testing performed
  • All existing tests pass

Test Environment

  • OS:
  • Node.js version:
  • Package manager:

Performance Impact

  • No performance impact
  • Performance improvement
  • Minor performance impact (acceptable)
  • Significant performance impact (needs discussion)

Breaking Changes

Screenshots/Demo

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

Summary by CodeRabbit

  • Bug Fixes
    • Clearer, provider-specific error messages across Amazon Bedrock, Anthropic, Azure OpenAI, Google AI Studio, Google Vertex, LiteLLM, Mistral, and OpenAI.
    • Explicit handling for rate limits (429), connection/network failures, server errors (5xx), and invalid API keys.
    • Consistent “Provider Error: …” formatting for unmatched cases to reduce ambiguity.
    • Mistral now shows a precise “model not found” message with guidance.
    • Overall improves reliability and user understanding of errors.

@coderabbitai

coderabbitai Bot commented Sep 25, 2025 •

Copy link
Copy Markdown

Walkthrough

Adds standardized, message-based error handling across multiple provider modules, introducing early-return branches for API key, rate limit, connection, and server errors. Fallback messages are unified per provider. Control flow now derives a single message string and routes to provider-specific normalized errors.

Changes

Cohort / File(s) Change summary
Provider error normalization
src/lib/providers/amazonBedrock.ts, src/lib/providers/anthropic.ts, src/lib/providers/azureOpenai.ts, src/lib/providers/googleAiStudio.ts, src/lib/providers/googleVertex.ts, src/lib/providers/litellm.ts, src/lib/providers/mistral.ts, src/lib/providers/openAI.ts
Standardizes handleProviderError: derive a single message; add early branches for API key (where applicable), rate limit (incl. 429), connection issues (ECONNRESET/ENOTFOUND/ECONNREFUSED/network/connection), and server errors (500/502/503/504/server error); return provider-specific, user-facing messages; unify fallback error prefixes. Minor provider-specific additions (e.g., model-not-found in Mistral).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Provider
  participant UpstreamAPI as Upstream API

  Client->>Provider: request(model, payload)
  Provider->>UpstreamAPI: send request
  UpstreamAPI--x Provider: error (status/message)
  rect rgba(230,240,255,0.5)
    note over Provider: Derive message string once
    alt API key invalid
      Provider-->>Client: Normalized "Invalid API key/endpoint" error
    else Rate limited (incl. 429)
      Provider-->>Client: Normalized "Rate limit exceeded" error
    else Connection issue
      Provider-->>Client: Normalized "API connection error" message
    else Server error (5xx)
      Provider-->>Client: Normalized "API server error" message
    else Provider-specific/fallback
      Provider-->>Client: "<Provider> Error: {message}"
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • murdore

Poem

A bunny taps logs with a gentle thump-thump,
Sorting errors into a tidy clump.
Rate-limits nibble, connections hop away—
Server clouds rumble, yet flows hold sway.
With whiskered wisdom and ears alert,
The stack now squeaks clear, with fewer hurt. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title indicates a refactor in a utils module to simplify error formatting, but the changes actually standardize and expand error handling across multiple provider implementations, so the title does not describe the main scope of the pull request. Update the pull request title to accurately reflect the main changes, for example “refactor(providers): standardize and normalize error handling across AI providers.”
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/providers/googleVertex.ts (1)

365-371: Bug: streamText result is not awaited (passes a Promise to createTextStream).

Await the streamText call before transforming the stream.

Apply this diff:

-      const result = streamText(streamOptions);
+      const result = await streamText(streamOptions);
🧹 Nitpick comments (9)
src/lib/providers/googleVertex.ts (3)

423-446: Normalize error matching and honor HTTP status codes.

Use a lowercased message and check optional status fields to make detection robust. Also include ETIMEDOUT/timeout and common gateway phrases.

Apply this diff:

-    if (
-      message.includes("ECONNRESET") ||
-      message.includes("ENOTFOUND") ||
-      message.includes("ECONNREFUSED") ||
-      message.includes("network") ||
-      message.includes("connection")
-    ) {
+    const lowerMessage = message.toLowerCase();
+    const status =
+      (errorRecord as { status?: number })?.status ??
+      (errorRecord as { response?: { status?: number } })?.response?.status;
+
+    if (
+      lowerMessage.includes("econnreset") ||
+      lowerMessage.includes("enotfound") ||
+      lowerMessage.includes("econnrefused") ||
+      lowerMessage.includes("etimedout") ||
+      lowerMessage.includes("network") ||
+      lowerMessage.includes("connection") ||
+      lowerMessage.includes("timeout")
+    ) {
       return new Error(
         "Google Vertex AI API connection error. Please check your internet connection and try again.",
       );
     }
 
-    if (
-      message.includes("500") ||
-      message.includes("502") ||
-      message.includes("503") ||
-      message.includes("504") ||
-      message.includes("server error")
-    ) {
+    if (
+      status === 500 ||
+      status === 502 ||
+      status === 503 ||
+      status === 504 ||
+      lowerMessage.includes("server error") ||
+      lowerMessage.includes("bad gateway") ||
+      lowerMessage.includes("gateway timeout")
+    ) {
       return new Error(
         "Google Vertex AI API server error. Please try again in a few moments.",
       );
     }

177-183: Doc nit: comment says WeakMap but a Map is used.

Align the comment with the implementation to avoid confusion.

Apply this diff:

-  // Memory-managed cache for model configuration lookups to avoid repeated calls
-  // Uses WeakMap for automatic cleanup and bounded LRU for recently used models
+  // Memory-managed cache for model configuration lookups to avoid repeated calls
+  // Uses a bounded LRU strategy with Map for recently used models

418-422: Optional: lowercase once at source.

For consistency with other providers and tests, compute a lowercase variant once and reuse in all checks.

src/lib/providers/openAI.ts (2)

97-131: Harden error detection: lowercase matching and status checks.

Normalize the message and include HTTP status where available for rate limit, connection, and server errors.

Apply this diff:

-    if (
-      message.includes("rate limit") ||
-      message.includes("rate_limit_exceeded") ||
-      message.includes("429")
-    ) {
+    const lower = message.toLowerCase();
+    const status =
+      (errorObj as { status?: number })?.status ??
+      (errorObj as { response?: { status?: number } })?.response?.status;
+
+    if (
+      status === 429 ||
+      lower.includes("rate limit") ||
+      lower.includes("rate_limit_exceeded") ||
+      lower.includes("429")
+    ) {
       return new Error("OpenAI rate limit exceeded. Please try again later.");
     }
 
-    // Handle connection errors
-    if (
-      message.includes("ECONNRESET") ||
-      message.includes("ENOTFOUND") ||
-      message.includes("ECONNREFUSED") ||
-      message.includes("network") ||
-      message.includes("connection")
-    ) {
+    // Handle connection errors
+    if (
+      lower.includes("econnreset") ||
+      lower.includes("enotfound") ||
+      lower.includes("econnrefused") ||
+      lower.includes("etimedout") ||
+      lower.includes("network") ||
+      lower.includes("connection") ||
+      lower.includes("timeout")
+    ) {
       return new Error(
         "OpenAI API connection error. Please check your internet connection and try again.",
       );
     }
 
-    // Handle server errors
-    if (
-      message.includes("500") ||
-      message.includes("502") ||
-      message.includes("503") ||
-      message.includes("504") ||
-      message.includes("server error")
-    ) {
+    // Handle server errors
+    if (
+      status === 500 ||
+      status === 502 ||
+      status === 503 ||
+      status === 504 ||
+      lower.includes("server error") ||
+      lower.includes("bad gateway") ||
+      lower.includes("gateway timeout")
+    ) {
       return new Error(
         "OpenAI API server error. Please try again in a few moments.",
       );
     }
 
-    return new Error(`OpenAI Error: ${message}`);
+    return new Error(`OpenAI Error: ${message}`);

82-87: Optional: precompute lowercase message
Compute const lowerMessage = message.toLowerCase() once after deriving message and use lowerMessage.includes(…) in the subsequent error-handling checks.

src/lib/providers/mistral.ts (1)

173-212: Harden error detection: lowercase matching and status checks.

Normalize the message and include HTTP status where available; also lower-case the model-not-found check.

Apply this diff:

-    if (
-      message.includes("rate limit") ||
-      message.includes("rate_limit_exceeded") ||
-      message.includes("429")
-    ) {
+    const lower = message.toLowerCase();
+    const status =
+      (errorRecord as { status?: number })?.status ??
+      (errorRecord as { response?: { status?: number } })?.response?.status;
+
+    if (
+      status === 429 ||
+      lower.includes("rate limit") ||
+      lower.includes("rate_limit_exceeded") ||
+      lower.includes("429")
+    ) {
       return new Error("Mistral rate limit exceeded. Please try again later.");
     }
 
-    if (
-      message.includes("ECONNRESET") ||
-      message.includes("ENOTFOUND") ||
-      message.includes("ECONNREFUSED") ||
-      message.includes("network") ||
-      message.includes("connection")
-    ) {
+    if (
+      lower.includes("econnreset") ||
+      lower.includes("enotfound") ||
+      lower.includes("econnrefused") ||
+      lower.includes("etimedout") ||
+      lower.includes("network") ||
+      lower.includes("connection") ||
+      lower.includes("timeout")
+    ) {
       return new Error(
         "Mistral API connection error. Please check your internet connection and try again.",
       );
     }
 
-    if (
-      message.includes("500") ||
-      message.includes("502") ||
-      message.includes("503") ||
-      message.includes("504") ||
-      message.includes("server error")
-    ) {
+    if (
+      status === 500 ||
+      status === 502 ||
+      status === 503 ||
+      status === 504 ||
+      lower.includes("server error") ||
+      lower.includes("bad gateway") ||
+      lower.includes("gateway timeout")
+    ) {
       return new Error(
         "Mistral API server error. Please try again in a few moments.",
       );
     }
 
-    if (message.includes("model") && message.includes("not found")) {
+    if (lower.includes("model") && lower.includes("not found")) {
       return new Error(
         `Mistral model '${this.modelName}' not found.\n\nSuggestions:\n1. Check model name spelling\n2. Ensure you have access to the model`,
       );
     }
 
-    return new Error(`Mistral Error: ${message}`);
+    return new Error(`Mistral Error: ${message}`);
src/lib/providers/amazonBedrock.ts (2)

163-196: Harden error detection: lowercase matching and status checks (AWS often sets $metadata.httpStatusCode).

Normalize the message and read status from multiple potential locations.

Apply this diff:

-    if (
-      errorMessage.includes("rate limit") ||
-      errorMessage.includes("throttling") ||
-      errorMessage.includes("429")
-    ) {
+    const lower = errorMessage.toLowerCase();
+    const status =
+      (error as { status?: number })?.status ??
+      (error as { $metadata?: { httpStatusCode?: number } })?.$metadata?.httpStatusCode ??
+      (error as { response?: { status?: number } })?.response?.status;
+
+    if (
+      status === 429 ||
+      lower.includes("rate limit") ||
+      lower.includes("throttling") ||
+      lower.includes("too many requests") ||
+      lower.includes("429")
+    ) {
       return new Error(
         "Amazon Bedrock rate limit exceeded. Please try again later.",
       );
     }
 
-    if (
-      errorMessage.includes("ECONNRESET") ||
-      errorMessage.includes("ENOTFOUND") ||
-      errorMessage.includes("ECONNREFUSED") ||
-      errorMessage.includes("network") ||
-      errorMessage.includes("connection")
-    ) {
+    if (
+      lower.includes("econnreset") ||
+      lower.includes("enotfound") ||
+      lower.includes("econnrefused") ||
+      lower.includes("etimedout") ||
+      lower.includes("network") ||
+      lower.includes("connection") ||
+      lower.includes("timeout")
+    ) {
       return new Error(
         "Amazon Bedrock API connection error. Please check your internet connection and try again.",
       );
     }
 
-    if (
-      errorMessage.includes("500") ||
-      errorMessage.includes("502") ||
-      errorMessage.includes("503") ||
-      errorMessage.includes("504") ||
-      errorMessage.includes("server error")
-    ) {
+    if (
+      status === 500 ||
+      status === 502 ||
+      status === 503 ||
+      status === 504 ||
+      lower.includes("server error") ||
+      lower.includes("bad gateway") ||
+      lower.includes("gateway timeout")
+    ) {
       return new Error(
         "Amazon Bedrock API server error. Please try again in a few moments.",
       );
     }

122-138: Consistency: add timeout/abortSignal to Bedrock streaming like other providers.

Adopt the same timeout controller pattern to avoid hanging requests.

Apply this diff:

-    try {
-      this.validateStreamOptions(options);
+    try {
+      this.validateStreamOptions(options);
+      const timeout = this.getTimeout(options);
+      const timeoutController = createTimeoutController(
+        timeout,
+        this.providerName,
+        "stream",
+      );
 
       // Build message array from options
       const messages = buildMessagesArray(options);
 
-      const result = await streamText({
+      const result = await streamText({
         model: this.model,
         messages: messages,
         maxTokens: options.maxTokens || DEFAULT_MAX_TOKENS,
         temperature: options.temperature,
+        abortSignal: timeoutController?.controller.signal,
       });
 
+      timeoutController?.cleanup();
src/lib/providers/anthropic.ts (1)

88-140: Consolidate to lowercase message and include HTTP status checks.

Improves robustness and aligns with other providers.

Apply this diff:

-    const message =
-      typeof errorRecord?.message === "string"
-        ? errorRecord.message
-        : "Unknown error";
+    const message =
+      typeof errorRecord?.message === "string"
+        ? errorRecord.message
+        : "Unknown error";
+    const lowerMessage = message.toLowerCase();
+    const status =
+      (errorRecord as { status?: number })?.status ??
+      (errorRecord as { response?: { status?: number } })?.response?.status;
@@
-    if (
-      message.includes("API_KEY_INVALID") ||
-      message.includes("Invalid API key")
-    ) {
+    if (
+      lowerMessage.includes("api_key_invalid") ||
+      lowerMessage.includes("invalid api key")
+    ) {
       return new Error(
         "Invalid Anthropic API key. Please check your ANTHROPIC_API_KEY environment variable.",
       );
     }
@@
-    if (
-      message.includes("rate limit") ||
-      message.includes("too_many_requests") ||
-      message.includes("429")
-    ) {
+    if (
+      status === 429 ||
+      lowerMessage.includes("rate limit") ||
+      lowerMessage.includes("too_many_requests") ||
+      lowerMessage.includes("429")
+    ) {
       return new Error(
         "Anthropic rate limit exceeded. Please try again later.",
       );
     }
@@
-    if (
-      message.includes("ECONNRESET") ||
-      message.includes("ENOTFOUND") ||
-      message.includes("ECONNREFUSED") ||
-      message.includes("network") ||
-      message.includes("connection")
-    ) {
+    if (
+      lowerMessage.includes("econnreset") ||
+      lowerMessage.includes("enotfound") ||
+      lowerMessage.includes("econnrefused") ||
+      lowerMessage.includes("etimedout") ||
+      lowerMessage.includes("network") ||
+      lowerMessage.includes("connection") ||
+      lowerMessage.includes("timeout")
+    ) {
       return new Error(
         "Anthropic API connection error. Please check your internet connection and try again.",
       );
     }
@@
-    if (
-      message.includes("500") ||
-      message.includes("502") ||
-      message.includes("503") ||
-      message.includes("504") ||
-      message.includes("server error")
-    ) {
+    if (
+      status === 500 ||
+      status === 502 ||
+      status === 503 ||
+      status === 504 ||
+      lowerMessage.includes("server error") ||
+      lowerMessage.includes("bad gateway") ||
+      lowerMessage.includes("gateway timeout")
+    ) {
       return new Error(
         "Anthropic API server error. Please try again in a few moments.",
       );
     }
 
-    return new Error(`Anthropic Error: ${message}`);
+    return new Error(`Anthropic Error: ${message}`);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 446e148 and 623985d.

📒 Files selected for processing (8)
  • src/lib/providers/amazonBedrock.ts (1 hunks)
  • src/lib/providers/anthropic.ts (4 hunks)
  • src/lib/providers/azureOpenai.ts (1 hunks)
  • src/lib/providers/googleAiStudio.ts (1 hunks)
  • src/lib/providers/googleVertex.ts (1 hunks)
  • src/lib/providers/litellm.ts (1 hunks)
  • src/lib/providers/mistral.ts (1 hunks)
  • src/lib/providers/openAI.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-17T17:55:15.261Z
Learnt from: RajuSudhar
PR: juspay/neurolink#173
File: src/lib/index.ts:16-16
Timestamp: 2025-09-17T17:55:15.261Z
Learning: In src/lib/types/providers.ts, ProviderConfig was renamed to AIModelProviderConfig to deduplicate type names, as there was an existing ProviderConfig type that better suited the "ProviderConfig" name. This was an intentional breaking change for better type organization.

Applied to files:

  • src/lib/providers/litellm.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/googleAiStudio.ts
📚 Learning: 2025-09-02T13:50:42.770Z
Learnt from: YasmeenOgo
PR: juspay/neurolink#145
File: src/lib/core/types.ts:0-0
Timestamp: 2025-09-02T13:50:42.770Z
Learning: The APIVersions enum in src/lib/core/types.ts now contains comprehensive API version constants for all major AI providers: Azure OpenAI (latest, stable, legacy), OpenAI (current, beta), Google AI (current, beta), and Anthropic (current). This centralization helps avoid API version drift across the codebase.

Applied to files:

  • src/lib/providers/openAI.ts
  • src/lib/providers/azureOpenai.ts
🧬 Code graph analysis (7)
src/lib/providers/anthropic.ts (1)
neurolink-demo/test-providers.js (1)
  • message (291-291)
src/lib/providers/litellm.ts (1)
neurolink-demo/test-providers.js (1)
  • message (291-291)
src/lib/providers/openAI.ts (1)
neurolink-demo/test-providers.js (1)
  • message (291-291)
src/lib/providers/googleAiStudio.ts (1)
neurolink-demo/test-providers.js (1)
  • message (291-291)
src/lib/providers/googleVertex.ts (1)
neurolink-demo/test-providers.js (1)
  • message (291-291)
src/lib/providers/mistral.ts (1)
neurolink-demo/test-providers.js (1)
  • message (291-291)
src/lib/providers/azureOpenai.ts (1)
neurolink-demo/test-providers.js (1)
  • message (291-291)

Comment on lines 82 to 128
const message =
errorObj?.message && typeof errorObj.message === "string"
? errorObj.message
: "Unknown error";
return new Error(`Azure OpenAI error: ${message}`);

if (message.includes("401")) {
return new Error("Invalid Azure OpenAI API key or endpoint.");
}

if (
message.includes("rate limit") ||
message.includes("rate_limit_exceeded") ||
message.includes("429")
) {
return new Error(
"Azure OpenAI rate limit exceeded. Please try again later.",
);
}

// Handle connection errors
if (
message.includes("ECONNRESET") ||
message.includes("ENOTFOUND") ||
message.includes("ECONNREFUSED") ||
message.includes("network") ||
message.includes("connection")
) {
return new Error(
"Azure OpenAI API connection error. Please check your internet connection and try again.",
);
}

// Handle server errors
if (
message.includes("500") ||
message.includes("502") ||
message.includes("503") ||
message.includes("504") ||
message.includes("server error")
) {
return new Error(
"Azure OpenAI API server error. Please try again in a few moments.",
);
}

return new Error(`Azure OpenAI Error: ${message}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Case-sensitive comparisons regress the new Azure error handling

Azure responses (and node-fetch errors) routinely capitalize phrases like "Network Error" or "Server Error". Because message is no longer lowercased, checks such as message.includes("network")/"server error" now miss, so we fall back to the generic "Azure OpenAI Error: …" for common scenarios that previously produced tailored guidance.

Lowercase the message once for comparison while keeping the original text for the returned error.

-    const message =
-      errorObj?.message && typeof errorObj.message === "string"
-        ? errorObj.message
-        : "Unknown error";
+    const rawMessage =
+      errorObj?.message && typeof errorObj.message === "string"
+        ? errorObj.message
+        : "Unknown error";
+    const normalizedMessage = rawMessage.toLowerCase();
 
-    if (message.includes("401")) {
+    if (normalizedMessage.includes("401")) {
       return new Error("Invalid Azure OpenAI API key or endpoint.");
     }
 
     if (
-      message.includes("rate limit") ||
-      message.includes("rate_limit_exceeded") ||
-      message.includes("429")
+      normalizedMessage.includes("rate limit") ||
+      normalizedMessage.includes("rate_limit_exceeded") ||
+      normalizedMessage.includes("429")
     ) {
       return new Error(
         "Azure OpenAI rate limit exceeded. Please try again later.",
       );
     }
 
     // Handle connection errors
     if (
-      message.includes("ECONNRESET") ||
-      message.includes("ENOTFOUND") ||
-      message.includes("ECONNREFUSED") ||
-      message.includes("network") ||
-      message.includes("connection")
+      normalizedMessage.includes("econnreset") ||
+      normalizedMessage.includes("enotfound") ||
+      normalizedMessage.includes("econnrefused") ||
+      normalizedMessage.includes("network") ||
+      normalizedMessage.includes("connection")
     ) {
       return new Error(
         "Azure OpenAI API connection error. Please check your internet connection and try again.",
       );
     }
 
     // Handle server errors
     if (
-      message.includes("500") ||
-      message.includes("502") ||
-      message.includes("503") ||
-      message.includes("504") ||
-      message.includes("server error")
+      normalizedMessage.includes("500") ||
+      normalizedMessage.includes("502") ||
+      normalizedMessage.includes("503") ||
+      normalizedMessage.includes("504") ||
+      normalizedMessage.includes("server error")
     ) {
       return new Error(
         "Azure OpenAI API server error. Please try again in a few moments.",
       );
     }
 
-    return new Error(`Azure OpenAI Error: ${message}`);
+    return new Error(`Azure OpenAI Error: ${rawMessage}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const message =
errorObj?.message && typeof errorObj.message === "string"
? errorObj.message
: "Unknown error";
return new Error(`Azure OpenAI error: ${message}`);
if (message.includes("401")) {
return new Error("Invalid Azure OpenAI API key or endpoint.");
}
if (
message.includes("rate limit") ||
message.includes("rate_limit_exceeded") ||
message.includes("429")
) {
return new Error(
"Azure OpenAI rate limit exceeded. Please try again later.",
);
}
// Handle connection errors
if (
message.includes("ECONNRESET") ||
message.includes("ENOTFOUND") ||
message.includes("ECONNREFUSED") ||
message.includes("network") ||
message.includes("connection")
) {
return new Error(
"Azure OpenAI API connection error. Please check your internet connection and try again.",
);
}
// Handle server errors
if (
message.includes("500") ||
message.includes("502") ||
message.includes("503") ||
message.includes("504") ||
message.includes("server error")
) {
return new Error(
"Azure OpenAI API server error. Please try again in a few moments.",
);
}
return new Error(`Azure OpenAI Error: ${message}`);
}
const rawMessage =
errorObj?.message && typeof errorObj.message === "string"
? errorObj.message
: "Unknown error";
const normalizedMessage = rawMessage.toLowerCase();
if (normalizedMessage.includes("401")) {
return new Error("Invalid Azure OpenAI API key or endpoint.");
}
if (
normalizedMessage.includes("rate limit") ||
normalizedMessage.includes("rate_limit_exceeded") ||
normalizedMessage.includes("429")
) {
return new Error(
"Azure OpenAI rate limit exceeded. Please try again later.",
);
}
// Handle connection errors
if (
normalizedMessage.includes("econnreset") ||
normalizedMessage.includes("enotfound") ||
normalizedMessage.includes("econnrefused") ||
normalizedMessage.includes("network") ||
normalizedMessage.includes("connection")
) {
return new Error(
"Azure OpenAI API connection error. Please check your internet connection and try again.",
);
}
// Handle server errors
if (
normalizedMessage.includes("500") ||
normalizedMessage.includes("502") ||
normalizedMessage.includes("503") ||
normalizedMessage.includes("504") ||
normalizedMessage.includes("server error")
) {
return new Error(
"Azure OpenAI API server error. Please try again in a few moments.",
);
}
return new Error(`Azure OpenAI Error: ${rawMessage}`);
🤖 Prompt for AI Agents
In src/lib/providers/azureOpenai.ts around lines 82 to 128, the error text
checks are case-sensitive because the message isn't lowercased; as a result
capitalized Azure/node-fetch messages like "Network Error" or "Server Error"
miss the specific branches. Create a lowercase copy of the message (e.g.,
messageLower = message.toLowerCase()) and use messageLower for all includes()
comparisons (401, rate limit, network/connection, server errors), but keep using
the original message when constructing the returned Error so the original casing
and details are preserved.

Comment on lines +77 to 119
const message =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";

if (message.includes("API_KEY_INVALID")) {
return new Error(
"Invalid Google AI API key. Please check your GOOGLE_AI_API_KEY environment variable.",
);
}

if (message.includes("RATE_LIMIT_EXCEEDED")) {
return new Error(
"Google AI rate limit exceeded. Please try again later.",
);
}

if (
typeof errorRecord?.message === "string" &&
errorRecord.message.includes("API_KEY_INVALID")
message.includes("ECONNRESET") ||
message.includes("ENOTFOUND") ||
message.includes("ECONNREFUSED") ||
message.includes("network") ||
message.includes("connection")
) {
return new Error(
"Invalid Google AI API key. Please check your GOOGLE_AI_API_KEY environment variable.",
"Google AI API connection error. Please check your internet connection and try again.",
);
}

if (
typeof errorRecord?.message === "string" &&
errorRecord.message.includes("RATE_LIMIT_EXCEEDED")
message.includes("500") ||
message.includes("502") ||
message.includes("503") ||
message.includes("504") ||
message.includes("server error")
) {
return new Error(
"Google AI rate limit exceeded. Please try again later.",
"Google AI API server error. Please try again in a few moments.",
);
}

const message =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";
return new Error(`Google AI error: ${message}`);
return new Error(`Google AI Error: ${message}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Case-sensitive matching breaks new error buckets

Similar to the LiteLLM refactor, we now rely on raw message strings, but most checks compare lowercase fragments like "network" / "server error" against messages that come back as "Network error" / "Server Error". These matches no longer fire, causing us to fall back to the generic "Google AI Error: …" and losing the standardized guidance.

Normalize the message once for comparisons while preserving the original text for the final wrapper.

-    const message =
-      typeof errorRecord?.message === "string"
-        ? errorRecord.message
-        : "Unknown error";
+    const rawMessage =
+      typeof errorRecord?.message === "string"
+        ? errorRecord.message
+        : "Unknown error";
+    const normalizedMessage = rawMessage.toLowerCase();
 
-    if (message.includes("API_KEY_INVALID")) {
+    if (normalizedMessage.includes("api_key_invalid")) {
       return new Error(
         "Invalid Google AI API key. Please check your GOOGLE_AI_API_KEY environment variable.",
       );
     }
 
-    if (message.includes("RATE_LIMIT_EXCEEDED")) {
+    if (normalizedMessage.includes("rate_limit_exceeded")) {
       return new Error(
         "Google AI rate limit exceeded. Please try again later.",
       );
     }
 
     if (
-      message.includes("ECONNRESET") ||
-      message.includes("ENOTFOUND") ||
-      message.includes("ECONNREFUSED") ||
-      message.includes("network") ||
-      message.includes("connection")
+      normalizedMessage.includes("econnreset") ||
+      normalizedMessage.includes("enotfound") ||
+      normalizedMessage.includes("econnrefused") ||
+      normalizedMessage.includes("network") ||
+      normalizedMessage.includes("connection")
     ) {
       return new Error(
         "Google AI API connection error. Please check your internet connection and try again.",
       );
     }
 
     if (
-      message.includes("500") ||
-      message.includes("502") ||
-      message.includes("503") ||
-      message.includes("504") ||
-      message.includes("server error")
+      normalizedMessage.includes("500") ||
+      normalizedMessage.includes("502") ||
+      normalizedMessage.includes("503") ||
+      normalizedMessage.includes("504") ||
+      normalizedMessage.includes("server error")
     ) {
       return new Error(
         "Google AI API server error. Please try again in a few moments.",
       );
     }
 
-    return new Error(`Google AI Error: ${message}`);
+    return new Error(`Google AI Error: ${rawMessage}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const message =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";
if (message.includes("API_KEY_INVALID")) {
return new Error(
"Invalid Google AI API key. Please check your GOOGLE_AI_API_KEY environment variable.",
);
}
if (message.includes("RATE_LIMIT_EXCEEDED")) {
return new Error(
"Google AI rate limit exceeded. Please try again later.",
);
}
if (
typeof errorRecord?.message === "string" &&
errorRecord.message.includes("API_KEY_INVALID")
message.includes("ECONNRESET") ||
message.includes("ENOTFOUND") ||
message.includes("ECONNREFUSED") ||
message.includes("network") ||
message.includes("connection")
) {
return new Error(
"Invalid Google AI API key. Please check your GOOGLE_AI_API_KEY environment variable.",
"Google AI API connection error. Please check your internet connection and try again.",
);
}
if (
typeof errorRecord?.message === "string" &&
errorRecord.message.includes("RATE_LIMIT_EXCEEDED")
message.includes("500") ||
message.includes("502") ||
message.includes("503") ||
message.includes("504") ||
message.includes("server error")
) {
return new Error(
"Google AI rate limit exceeded. Please try again later.",
"Google AI API server error. Please try again in a few moments.",
);
}
const message =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";
return new Error(`Google AI error: ${message}`);
return new Error(`Google AI Error: ${message}`);
}
const rawMessage =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";
const normalizedMessage = rawMessage.toLowerCase();
if (normalizedMessage.includes("api_key_invalid")) {
return new Error(
"Invalid Google AI API key. Please check your GOOGLE_AI_API_KEY environment variable.",
);
}
if (normalizedMessage.includes("rate_limit_exceeded")) {
return new Error(
"Google AI rate limit exceeded. Please try again later.",
);
}
if (
normalizedMessage.includes("econnreset") ||
normalizedMessage.includes("enotfound") ||
normalizedMessage.includes("econnrefused") ||
normalizedMessage.includes("network") ||
normalizedMessage.includes("connection")
) {
return new Error(
"Google AI API connection error. Please check your internet connection and try again.",
);
}
if (
normalizedMessage.includes("500") ||
normalizedMessage.includes("502") ||
normalizedMessage.includes("503") ||
normalizedMessage.includes("504") ||
normalizedMessage.includes("server error")
) {
return new Error(
"Google AI API server error. Please try again in a few moments.",
);
}
return new Error(`Google AI Error: ${rawMessage}`);
🤖 Prompt for AI Agents
In src/lib/providers/googleAiStudio.ts around lines 77 to 119, the error
handling compares fragments in a case-sensitive way against the raw message so
checks like "network" and "server error" miss matches like "Network error" or
"Server Error"; normalize the message once (e.g., to lower-case) for all
substring comparisons while keeping the original message intact for the final
Error text, and update each if condition to test the normalized string instead
of the raw message.

Comment on lines +106 to 154
const message =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";

if (
errorRecord?.name === "TimeoutError" ||
(typeof errorRecord?.message === "string" &&
errorRecord.message.includes("Timeout"))
message.includes("ECONNREFUSED") ||
message.includes("Failed to fetch") ||
message.includes("ECONNRESET") ||
message.includes("ENOTFOUND")
) {
return new Error(
`LiteLLM request timed out: ${errorRecord?.message || "Unknown timeout"}`,
`LiteLLM proxy server not available. Please start the LiteLLM proxy server at ${
process.env.LITELLM_BASE_URL || "http://localhost:4000"
}`,
);
}
if (typeof errorRecord?.message === "string") {
if (
errorRecord.message.includes("ECONNREFUSED") ||
errorRecord.message.includes("Failed to fetch")
) {
return new Error(
"LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
`${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`,
);
}

if (
errorRecord.message.includes("API_KEY_INVALID") ||
errorRecord.message.includes("Invalid API key")
) {
return new Error(
"Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.",
);
}
if (
message.includes("API_KEY_INVALID") ||
message.includes("Invalid API key")
) {
return new Error(
"Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.",
);
}

if (errorRecord.message.includes("rate limit")) {
return new Error(
"LiteLLM rate limit exceeded. Please try again later.",
);
}
if (
message.includes("rate limit") ||
message.includes("rate_limit_exceeded") ||
message.includes("429")
) {
return new Error("LiteLLM rate limit exceeded. Please try again later.");
}

if (
errorRecord.message.includes("model") &&
errorRecord.message.includes("not found")
) {
return new Error(
`Model '${this.modelName}' not available in LiteLLM proxy. ` +
"Please check your LiteLLM configuration and ensure the model is configured.",
);
}
if (
message.includes("500") ||
message.includes("502") ||
message.includes("503") ||
message.includes("504") ||
message.includes("server error")
) {
return new Error(
"LiteLLM proxy server error. Please check the proxy server logs for more details.",
);
}

return new Error(
`LiteLLM error: ${errorRecord?.message || "Unknown error"}`,
);
return new Error(`LiteLLM Error: ${message}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore case-insensitive error routing

message is left in its original casing, yet most of the subsequent includes(...) checks use lowercase tokens (e.g., "rate limit", "server error"). With real LiteLLM responses like "Rate limit exceeded" or "Server Error", those checks now silently miss and we fall back to the generic "LiteLLM Error: …" wrapper, undoing the standardized messaging this refactor set out to provide.

Normalize the string once and perform the matches against that lowercase representation while keeping the original message for the final wrapper.

-    const message =
-      typeof errorRecord?.message === "string"
-        ? errorRecord.message
-        : "Unknown error";
+    const rawMessage =
+      typeof errorRecord?.message === "string"
+        ? errorRecord.message
+        : "Unknown error";
+    const normalizedMessage = rawMessage.toLowerCase();
 
-    if (
-      message.includes("ECONNREFUSED") ||
-      message.includes("Failed to fetch") ||
-      message.includes("ECONNRESET") ||
-      message.includes("ENOTFOUND")
+    if (
+      normalizedMessage.includes("econnrefused") ||
+      normalizedMessage.includes("failed to fetch") ||
+      normalizedMessage.includes("econnreset") ||
+      normalizedMessage.includes("enotfound")
     ) {
       return new Error(
         `LiteLLM proxy server not available. Please start the LiteLLM proxy server at ${
           process.env.LITELLM_BASE_URL || "http://localhost:4000"
         }`,
       );
     }
 
-    if (
-      message.includes("API_KEY_INVALID") ||
-      message.includes("Invalid API key")
-    ) {
+    if (
+      normalizedMessage.includes("api_key_invalid") ||
+      normalizedMessage.includes("invalid api key")
+    ) {
       return new Error(
         "Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.",
       );
     }
 
-    if (
-      message.includes("rate limit") ||
-      message.includes("rate_limit_exceeded") ||
-      message.includes("429")
-    ) {
+    if (
+      normalizedMessage.includes("rate limit") ||
+      normalizedMessage.includes("rate_limit_exceeded") ||
+      normalizedMessage.includes("429")
+    ) {
       return new Error("LiteLLM rate limit exceeded. Please try again later.");
     }
 
-    if (
-      message.includes("500") ||
-      message.includes("502") ||
-      message.includes("503") ||
-      message.includes("504") ||
-      message.includes("server error")
-    ) {
+    if (
+      normalizedMessage.includes("500") ||
+      normalizedMessage.includes("502") ||
+      normalizedMessage.includes("503") ||
+      normalizedMessage.includes("504") ||
+      normalizedMessage.includes("server error")
+    ) {
       return new Error(
         "LiteLLM proxy server error. Please check the proxy server logs for more details.",
       );
     }
 
-    return new Error(`LiteLLM Error: ${message}`);
+    return new Error(`LiteLLM Error: ${rawMessage}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const message =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";
if (
errorRecord?.name === "TimeoutError" ||
(typeof errorRecord?.message === "string" &&
errorRecord.message.includes("Timeout"))
message.includes("ECONNREFUSED") ||
message.includes("Failed to fetch") ||
message.includes("ECONNRESET") ||
message.includes("ENOTFOUND")
) {
return new Error(
`LiteLLM request timed out: ${errorRecord?.message || "Unknown timeout"}`,
`LiteLLM proxy server not available. Please start the LiteLLM proxy server at ${
process.env.LITELLM_BASE_URL || "http://localhost:4000"
}`,
);
}
if (typeof errorRecord?.message === "string") {
if (
errorRecord.message.includes("ECONNREFUSED") ||
errorRecord.message.includes("Failed to fetch")
) {
return new Error(
"LiteLLM proxy server not available. Please start the LiteLLM proxy server at " +
`${process.env.LITELLM_BASE_URL || "http://localhost:4000"}`,
);
}
if (
errorRecord.message.includes("API_KEY_INVALID") ||
errorRecord.message.includes("Invalid API key")
) {
return new Error(
"Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.",
);
}
if (
message.includes("API_KEY_INVALID") ||
message.includes("Invalid API key")
) {
return new Error(
"Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.",
);
}
if (errorRecord.message.includes("rate limit")) {
return new Error(
"LiteLLM rate limit exceeded. Please try again later.",
);
}
if (
message.includes("rate limit") ||
message.includes("rate_limit_exceeded") ||
message.includes("429")
) {
return new Error("LiteLLM rate limit exceeded. Please try again later.");
}
if (
errorRecord.message.includes("model") &&
errorRecord.message.includes("not found")
) {
return new Error(
`Model '${this.modelName}' not available in LiteLLM proxy. ` +
"Please check your LiteLLM configuration and ensure the model is configured.",
);
}
if (
message.includes("500") ||
message.includes("502") ||
message.includes("503") ||
message.includes("504") ||
message.includes("server error")
) {
return new Error(
"LiteLLM proxy server error. Please check the proxy server logs for more details.",
);
}
return new Error(
`LiteLLM error: ${errorRecord?.message || "Unknown error"}`,
);
return new Error(`LiteLLM Error: ${message}`);
}
const rawMessage =
typeof errorRecord?.message === "string"
? errorRecord.message
: "Unknown error";
const normalizedMessage = rawMessage.toLowerCase();
if (
normalizedMessage.includes("econnrefused") ||
normalizedMessage.includes("failed to fetch") ||
normalizedMessage.includes("econnreset") ||
normalizedMessage.includes("enotfound")
) {
return new Error(
`LiteLLM proxy server not available. Please start the LiteLLM proxy server at ${
process.env.LITELLM_BASE_URL || "http://localhost:4000"
}`,
);
}
if (
normalizedMessage.includes("api_key_invalid") ||
normalizedMessage.includes("invalid api key")
) {
return new Error(
"Invalid LiteLLM configuration. Please check your LITELLM_API_KEY environment variable.",
);
}
if (
normalizedMessage.includes("rate limit") ||
normalizedMessage.includes("rate_limit_exceeded") ||
normalizedMessage.includes("429")
) {
return new Error("LiteLLM rate limit exceeded. Please try again later.");
}
if (
normalizedMessage.includes("500") ||
normalizedMessage.includes("502") ||
normalizedMessage.includes("503") ||
normalizedMessage.includes("504") ||
normalizedMessage.includes("server error")
) {
return new Error(
"LiteLLM proxy server error. Please check the proxy server logs for more details.",
);
}
return new Error(`LiteLLM Error: ${rawMessage}`);
🤖 Prompt for AI Agents
In src/lib/providers/litellm.ts around lines 106 to 154, the code performs
substring matches against message but leaves message in original casing causing
case-sensitive misses; create a lowercase copy once (e.g., const lower =
message.toLowerCase()) and use lower.includes(...) for all subsequent checks
(ECONNREFUSED/failed fetch/invalid key/rate limit/server error checks) while
preserving and using the original message string in the final returned Error
wrapper.

@murdore
murdore requested a review from Copilot September 30, 2025 16:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors error handling across AI provider classes to improve error message clarity and consistency. The changes simplify the error formatting logic while providing more specific, user-friendly error messages.

Key changes:

  • Standardized error handling patterns across all provider classes
  • Added comprehensive error detection for rate limits, connection issues, and server errors
  • Simplified message extraction logic by moving it earlier in error handling functions
  • Enhanced provider-specific error messages with consistent "Provider Error:" formatting

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/lib/providers/openAI.ts Added comprehensive error handling for connection and server errors with improved rate limit detection
src/lib/providers/mistral.ts Enhanced error handling with model-not-found detection and consistent error formatting
src/lib/providers/litellm.ts Simplified error message extraction and improved server error handling
src/lib/providers/googleVertex.ts Added connection and server error handling to existing permission logic
src/lib/providers/googleAiStudio.ts Reorganized error handling order and added comprehensive error type detection
src/lib/providers/azureOpenai.ts Restructured error handling with improved message extraction and consistent patterns
src/lib/providers/anthropic.ts Simplified error message extraction logic and improved code readability
src/lib/providers/amazonBedrock.ts Added rate limit, connection, and server error handling to existing error logic

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


if (message.includes("model") && message.includes("not found")) {
return new Error(
`Mistral model '${this.modelName}' not found.\n\nSuggestions:\n1. Check model name spelling\n2. Ensure you have access to the model`,

Copilot AI Sep 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The multiline error message with embedded newlines reduces readability. Consider using template literals with proper line breaks or extracting this to a constant.

Suggested change
`Mistral model '${this.modelName}' not found.\n\nSuggestions:\n1. Check model name spelling\n2. Ensure you have access to the model`,
`Mistral model '${this.modelName}' not found.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to 111

if (

Copilot AI Sep 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout error handling that was removed (lines checking for TimeoutError and Timeout in message) should be preserved as timeouts are a distinct error condition that users should be able to identify.

Suggested change
if (
// Check for timeout errors in the message
if (
message.includes("TimeoutError") ||
message.toLowerCase().includes("timeout") ||
message.toLowerCase().includes("timed out")
) {
return new Error(`LiteLLM request timed out: ${message}`);
}
if (

Copilot uses AI. Check for mistakes.
@murdore

murdore commented Mar 29, 2026

Copy link
Copy Markdown
Contributor

Closing as part of project audit (2026-03-29). The feature this PR targets was implemented via a different approach in a later release. See docs/project-audit-2026-03-29.md for details.

@murdore murdore closed this Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants